#!/bin/bash

# requirements:
# jq to parse json
# curl
# coreutils: sort, cat...

me="${0##*/}"

usage() {
    [[ "$@" != "" ]] && printf "%s\n" "$@"
    cat <<EOF

This script will fetch a current list of DNS servers from $host,
filter them by countries if desired, sort by percentage, and write the result
to files readable by various resolvers.
It will then restart some systemd services if they are active. If the systemctl
executable is not found in PATH, the script will gracefully exit instead.

The script takes no command line options (except "-h" which produces this
output, everything else is discarded), but searches for a config file in
"\$HOME/.config/opennic-resolve/opennic-resolve.conf"
"/etc/opennic-resolve/opennic-resolve.conf"
"\$0.conf" i.e. the full path to the script + .conf
"\$PWD/opennic-resolve.conf" i.e. in the present working directory
Whichever is encountered first will be the only one to parse.

Defaults and possible configuration:

countries=  Provide a comma-separated list of 2-letter country codes.
            Nameservers from other countries won't be added to the file.
            Empty by default - no filtering by country.

max=        Maximum number of entries to write to a file. Default: $max
            Should be between $minmax and $maxmax for the API query, although it
            appears that it rarely returns more than 20 results anyhow.

min=        Minimum number of entries to write to a file. Default: $min
            If fewer nameservers are found no file will be overwritten.

ipv=        Specify which class of IP addresses to return.
            4   = Only list IPv4 servers
            6   = Only list IPv6 servers
            all = Include both IPv4 and IPv6 servers
            64  = Include all server, but list IPv6 address when both exist
            (unspecified by default)

timeout=    Timeout in s for data fetching from $host. default: $timeout

out=        Comma-separated list of actions. Possible values:
            resolvconf
                each line is either a comment starting with "#" or
                "nameserver " followed by the IP of the nameserver
            resolved
                starts with "[Resolve]", after which each line is either a
                comment starting with "#" or "DNS=" followed by the IP
                of the nameserver
            networkd
                starts with "[Network]", after which each line is either a
                comment starting with "#" or "DNS=" followed by the IP
                of the nameserver
            Overrides the default, which is "resolvconf,resolved"

resolvconf= Write to this file instead of $resolvconf
networkd=   Write networkd syntax to this file (no default)
resolved=   Write to this file instead of $resolved

services=   Comma-separated list of systemd services to restart, if active.
            Overrides the default, which is:
            "$services"

More information:
EOF
    echo "https://"{notabug,framagit}".org/ohnonot/opennic"
    exit 1
}
rs_format() {
    # $1: prefix - "DNS=" for systemd files, "nameserver " for /etc/resolv.conf
    # reverse sort by percentage, split into 2 lines for each nameserver:
    # line1: prefix ip
    # line2: # percentage # location
    i=0
    printf "$rs"| sort -r | while read -a line && (( i++ < max )); do
        printf "# %s %s %s%%\n%s%s\n" "${line[2]}" "${line[3]}" "${line[0]}" "$1" "${line[1]}"
    done
}
rs_grow() {
    # add entries to results string rs
    local i="$1"
    ip="$(jq -Mr ".[$i] .ip" <<<"$data")"
    c="$(jq -Mj ".[$i] .loc" <<<"$data")"
    c="${c##* }"
    rs="${rs}$(jq -Mj ".[$i] .stat" <<<"$data") $ip $c $(jq -Mj ".[$i] .host" <<<"$data")\n"
    ((ns++))
}

# to be filled with namserver entries for /etc/resolv.conf
rs=""
# a counter for chosen nameserver results
ns=0

countries=""
out=""
services="systemd-networkd.service,systemd-resolved.service"

config=''
config_paths=( "$HOME/.config/$me/$me.conf" "/etc/$me/$me.conf" "$0.conf" "$PWD/$me.conf" )

resolvconf="/etc/resolv.conf"
# This one is just an example:
# networkd="/etc/systemd/network/20-wired.network.d/dns.conf"
networkd=""
resolved="/etc/systemd/resolved.conf.d/dns.conf"

do_resolvconf=1
do_resolved=1
do_networkd=0

# maximum of results written to file
max=5
# limits for the above:
minmax=2
maxmax=50
# minimum of results written to file
# i.e., if there's less nameservers than this, no file will be overwritten
min=2
# timeout for curl data fetching
timeout=10
# no preference for IP version. user configurable
ipv=

host="api.opennicproject.org"
#~ api_ip=161.97.219.82
# json format, ipv6 over 4, min. 99% (doesn't seem to work), 20 results
url="https://$host/geoip/?json&pct=99&res=$maxmax"
# just give us 20 of whatever you think is best, in json:
#~ url="https://$host/geoip/?json&res=$maxmax"

[[ "$1" == "-h" || "$1" == "--help" ]] && usage

# dependency checks
for dep in jq curl sort; do
    type -f $dep >/dev/null || exit 1
done
####################### USER CONF #############################################
for config in "${config_paths[@]}"; do
    if [ -r "$config" ]; then
        echo "Reading configuration from $config" >&2
        break
    fi
done
##### Parse config file #####
shopt -s extglob
if [ -r "$config" ]; then
    while IFS='= ' read -r lhs rhs; do
        if [[ ! $lhs =~ ^\ *# && -n $lhs ]]; then
            rhs="${rhs%%\#*}"    # Del in line right comments
            rhs="${rhs%%*( )}"   # Del trailing spaces
            rhs="${rhs%\"*}"     # Del opening string quotes 
            rhs="${rhs#\"*}"     # Del closing string quotes 
            declare $lhs="$rhs"
            echo "${lhs}=${!lhs}"
        fi
    done <"$config"
fi
unset config_paths config rhs lhs
echo
##### Quality Check for user config #####
[ "$max" -ge "$minmax" ] && [ "$max" -le "$maxmax" ] || usage "max must be larger than or equal to $minmax, and smaller than or equal to $maxmax."
[ "$min" -gt 0 ] || usage "min must be larger than 0."
[ "$timeout" -gt 0 ] || usage "timeout must be larger than 0."
if [ -n "$ipv" ]; then
    [[ "$ipv" == 6 || "$ipv" == 4 || "$ipv" == "all" || "$ipv" == 64 ]]\
    || usage "wrong value for ipv: $ipv"
fi
(( min > max )) && usage "Minimum $min cannot be larger than maximum $max."
if [[ "$out" != "" ]]; then
    out=( ${out//,/ } )
    do_resolvconf=0
    do_resolved=0
    do_networkd=0
    for i in "${out[@]}"; do
        case "$i" in
            resolvconf) do_resolvconf=1
            ;;
            resolved) do_resolved=1
            ;;
            networkd) do_networkd=1
            ;;
        esac
    done
fi
unset out

if [[ "$countries" != "" ]]; then
    countries=( ${countries//,/ } )
    for ((i=0;i<${#countries[@]};i++)); do
        countries[i]="${countries[i],,}"
        [[ "${countries[i]}" == [a-z][a-z] ]] || usage "Invalid country ${countries[i]}"
    done
else
    unset countries
fi
services=( ${services//,/ } )
for i in "${services[@]}"; do
    [[ "$i" == *\.service ]] || usage "Invalid service $i"
done

####################### END USER CONF #########################################

curlopts=(
    #~ --resolve "${host}:443:${api_ip}"
    --connect-timeout "$timeout"
    --max-time "$timeout"
)

# json data of nameservers, obtained from opennic
printf "Fetching %s\n" "$url"
data="$(curl "${curlopts[@]}" "$url")"

# fill array with all nameservers locations 
oldifs="$IFS";IFS=$'\n'
loc=( $(jq -Mr '.[] .loc' <<<"$data") )
IFS="$oldifs"

# check locations against whitelist of allowed countries...
for (( i=0 ; i<${#loc[@]} ; i++ )); do
    if (( ${#countries[@]} > 0 )); then
    # if countries are restricted, filter for suitable results
        for (( j=0 ; j<${#countries[@]} ; j++ )); do
            loc[i]="${loc[i],,}" # lowercase for comparison
            if [[ "${loc[i]##* }" == "${countries[j]}" ]]; then
                rs_grow "$i"
                break
            fi
        done
    else
        rs_grow "$i"
    fi
done

echo -e "\nRaw list: $ns results.\n$rs" >&2

# how many results did we get?
(( ns < min )) && echo "Not enough IPs for this configuration. Nothing changed." && exit 1

if [[ "$do_resolvconf" == 1 ]] && [ -d "${resolvconf%/*}" ]; then
rs_format "nameserver " > "$resolvconf"
fi

if [[ "$do_networkd" == 1 ]] && [ -d "${networkd%/*}" ]; then
echo "[Network]" > "$networkd"
rs_format "DNS=" >> "$networkd"
fi

if [[ "$do_resolved" == 1 ]] && [ -d "${resolved%/*}" ]; then
echo "[Resolve]" > "$resolved"
rs_format "DNS=" >> "$resolved"
fi

type -f systemctl >/dev/null || exit 0
# Restart systemd services if active
# According to wiki.archlinux.org/index.php/Domain_name_resolution, dhcpcd uses
# the glibc resolver which reads /etc/resolv.conf for every name resolution
# Thus, a dhcpcd.service restart should not be required.
for s in "${services[@]}"; do
    systemctl --quiet is-active "$s" && systemctl restart "$s"
done

exit 0
